/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

// Cribbed heavily from MSDN magazine's .Net Matters (12/2005) by Stephen Toub.
// http://msdn.microsoft.com/msdnmag/issues/05/12/NETMatters/
// This started off an excellent implementation, I'd rather not unnecessarily rewrite it.
// Some small stylistic changes were made to make it more consistent with the rest of the code
// and also changed the recurse criteria.

namespace C4F.DevKit.Contacts.Interop
{
    using System;
    using System.ComponentModel;
    using System.IO;
    using System.Security;
    using System.Collections.Generic;
    using System.Security.Permissions;
    using Microsoft.Win32.SafeHandles;
    using System.Runtime.ConstrainedExecution;
    using System.Runtime.InteropServices;
    using C4F.DevKit.Contacts.Standard;

    internal class FileWalker
    {
        private sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
            private SafeFindHandle() : base(true) { }

            protected override bool ReleaseHandle()
            {
                return FindClose(this.handle);
            }

            [DllImport("kernel32.dll")]
            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
            [SuppressUnmanagedCodeSecurity]
            private static extern bool FindClose(IntPtr handle);
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [SuppressUnmanagedCodeSecurity]
        private static extern SafeFindHandle FindFirstFile(string lpFileName, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [SuppressUnmanagedCodeSecurity]
        private static extern bool FindNextFile(SafeFindHandle hndFindFile, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern ErrorModes SetErrorMode(ErrorModes newMode);

        [Serializable]
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        [BestFitMapping(false)]
        private class WIN32_FIND_DATA
        {
            public FileAttributes dwFileAttributes;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
            public int nFileSizeHigh;
            public int nFileSizeLow;
            public int dwReserved0;
            public int dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }

        [Flags]
        private enum ErrorModes
        {
            /// <summary>Use the system default, which is to display all error dialog boxes.</summary>
            Default = 0x0,
            /// <summary>
            /// The system does not display the critical-error-handler message box. 
            /// Instead, the system sends the error to the calling process.
            /// </summary>
            FailCriticalErrors = 0x1,
            /// <summary>
            /// 64-bit Windows:  The system automatically fixes memory alignment faults and makes them 
            /// invisible to the application. It does this for the calling process and any descendant processes.
            /// After this value is set for a process, subsequent attempts to clear the value are ignored.
            /// </summary>
            NoGpFaultErrorBox = 0x2,
            /// <summary>
            /// The system does not display the general-protection-fault message box. 
            /// This flag should only be set by debugging applications that handle general 
            /// protection (GP) faults themselves with an exception handler.
            /// </summary>
            NoAlignmentFaultExcept = 0x4,
            /// <summary>
            /// The system does not display a message box when it fails to find a file. 
            /// Instead, the error is returned to the calling process.
            /// </summary>
            NoOpenFileErrorBox = 0x8000
        }

        public static IEnumerable<FileInfo> GetFiles(DirectoryInfo dir, string pattern)
        {
            // We suppressed this demand for each p/invoke call, so demand it upfront once
            new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand();

            // Validate parameters
            Validate.NotNull(dir, "dir");
            Validate.NeitherNullNorEmpty(pattern, "pattern");

            // Setup
            WIN32_FIND_DATA findData = new WIN32_FIND_DATA();
            Stack<DirectoryInfo> directories = new Stack<DirectoryInfo>();
            directories.Push(dir);

            // Process each directory
            ErrorModes origErrorMode = SetErrorMode(ErrorModes.FailCriticalErrors);
            try
            {
                while (directories.Count > 0)
                {
                    // Get the name of the next directory and the corresponding search pattern
                    dir = directories.Pop();
                    string dirPath = dir.FullName.Trim();
                    if (dirPath.Length == 0)
                    {
                        continue;
                    }
                    char lastChar = dirPath[dirPath.Length - 1];
                    if (lastChar != Path.DirectorySeparatorChar && lastChar != Path.AltDirectorySeparatorChar)
                    {
                        dirPath += Path.DirectorySeparatorChar;
                    }

                    // Process all files in that directory
                    SafeFindHandle handle = FindFirstFile(dirPath + pattern, findData);
                    if (handle.IsInvalid)
                    {
                        Win32Error error = Win32Error.GetLastError();
                        if (error == Win32Error.ERROR_ACCESS_DENIED || error == Win32Error.ERROR_FILE_NOT_FOUND)
                        {
                            continue;
                        }
                        Assert.AreNotEqual(Win32Error.ERROR_SUCCESS, error);
                        ((HRESULT)error).ThrowIfFailed();
                    }
                    else
                    {
                        try
                        {
                            do
                            {
                                if ((findData.dwFileAttributes & FileAttributes.Directory) == 0)
                                {
                                    yield return new FileInfo(dirPath + findData.cFileName);
                                }
                            }
                            while (FindNextFile(handle, findData));
                            Win32Error error = Win32Error.GetLastError();
                            if (error != Win32Error.ERROR_NO_MORE_FILES)
                            {
                                ((HRESULT)error).ThrowIfFailed();
                            }
                        }
                        finally
                        { 
                            handle.Dispose(); 
                        }
                    }

                    foreach (DirectoryInfo childDir in dir.GetDirectories())
                    {
                        // If it's not a hidden, system folder
                        if ((File.GetAttributes(childDir.FullName) & (FileAttributes.Hidden | FileAttributes.System)) == 0)
                        {
                            // And it's not a reparse point
                            if ((File.GetAttributes(childDir.FullName) & FileAttributes.ReparsePoint) == 0)
                            {
                                directories.Push(childDir);
                            }
                        }
                    }
                }
            }
            finally
            { 
                SetErrorMode(origErrorMode);
            }
        }
    }
}
